﻿using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using ZFY.Core;
using ZFY.UnitOfWork.Interface;

namespace ZFY.EntityFrameworkCore
{
    public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>,ISignleDependency
       where TDbContext : DbContext
    {
        public ILogger<UnitOfWorkDbContextProvider<TDbContext>> Logger { get; set; }

        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public UnitOfWorkDbContextProvider(
            IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;

            Logger = NullLogger<UnitOfWorkDbContextProvider<TDbContext>>.Instance;
        }

        

        public async Task<TDbContext> GetDbContextAsync()
        {
            var unitOfWork = _unitOfWorkManager.Current;
            if (unitOfWork == null)
            {
                throw new InvalidOperationException("A DbContext can only be created inside a unit of work!");
            }

         
            var dbContextKey = $"{typeof(TDbContext).FullName}";

            var dbContextApi = unitOfWork.FindDbContext(dbContextKey);

            if (dbContextApi == null)
            {
                dbContextApi = new EFDbContextApi(await CreateDbContextAsync(unitOfWork));

                unitOfWork.AddDbContext(dbContextKey, dbContextApi);
            }

            return (TDbContext)((EFDbContextApi)dbContextApi).DbContext;
        }


        private async Task<TDbContext> CreateDbContextAsync(IZFYUnitOfWork unitOfWork)
        {
            return unitOfWork.Options.Scope != System.Transactions.TransactionScopeOption.Suppress
                ? await CreateDbContextWithTransactionAsync(unitOfWork)
                : unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
            
        }

        private async Task<TDbContext> CreateDbContextWithTransactionAsync(IZFYUnitOfWork unitOfWork)
        {
            var transactionApiKey = $"Transaction_{typeof(TDbContext).FullName}";
            var activeTransaction = unitOfWork.FindDbTransaction(transactionApiKey) as EfCoreTransactionApi;

            if (activeTransaction == null)
            {
                
                var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();

                var dbTransaction = unitOfWork.Options.IsolationLevel.HasValue
                    ? await dbContext.Database.BeginTransactionAsync(unitOfWork.Options.IsolationLevel.Value, GetCancellationToken())
                    : await dbContext.Database.BeginTransactionAsync(GetCancellationToken());



                unitOfWork.AddDbTransaction(transactionApiKey, new EfCoreTransactionApi(dbTransaction,dbContext));

                return dbContext;
            }
            else
            {
                var dbConnection = activeTransaction.DbContextTransaction.GetDbTransaction().Connection;

                var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();

                if (dbContext.Database.GetDbConnection() == dbConnection)
                {
                    await dbContext.Database.UseTransactionAsync(activeTransaction.DbContextTransaction.GetDbTransaction(), GetCancellationToken());
                }
                else
                {
                    /* User did not re-use the ExistingConnection and we are starting a new transaction.
                     * EfCoreTransactionApi will check the connection string match and separately
                     * commit/rollback this transaction over the DbContext instance. */
                    if (unitOfWork.Options.IsolationLevel.HasValue)
                    {
                        await dbContext.Database.BeginTransactionAsync(
                            unitOfWork.Options.IsolationLevel.Value,
                            GetCancellationToken()
                        );
                    }
                    else
                    {
                        await dbContext.Database.BeginTransactionAsync(
                            GetCancellationToken()
                        );
                    }
                }

                activeTransaction.AttendedDbContexts.Add(dbContext);

                return dbContext;
            }
        }


        protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
        {
            return new CancellationToken();
        }
    }
}
